Rails要建立關聯非常簡單(one-to-one, one-to-many),但也是這個原因,造成在資料庫查詢的時候浪費許多記憶體,大部分的 ORM 預設使用 lazy-loading,一筆資料的查詢就會產生一筆 query,拖累了資料庫的效能。因此將預設的 lazy-loading 改成 Eager-loading 就可以解決N+1問題。
其中 Eager-loading在 rails 提供了4種方式:
這篇就拿最常見的inclundes及joins來討論。
我們先建立兩個Model分別為user.rb
及product.rb
且為一對多關係:
#user.rb
class User < ApplicationRecord
has_many :products
end
#product.rb
class Product < ApplicationRecord
belongs_to :user
end
controller先將所有的Pdoduct撈出:
#products_controller.rb
def index
@products = Product.all
end
再一筆一筆印出來:
<!-- index.erb.html -->
<% @products.each do |product| %>
<%= product.user.name%>
<%= product.title%>
<%= product.price%>
....
<% end %>
乍看之下好像很合理,如果說只有一筆product要查這樣寫沒問題,但一個網站怎麼可能只有一筆資料?
若今天要找出product的擁有者、名字及價錢有10筆。在迭代每筆資料時,依邊呼叫 query 來從 Prdoduct 資料表中取資料,就會產生 10 + 1 次 query,其中後面的 1 指的是 User資料數量。
資料的存取是rails的弱項,所以在設計時能夠避免 N+1問題就避免,將資料存取相關的事交給擅長的資料庫就好。
拿上面例子來看,會產生 10+1筆的資料存取,我們可以用includes的方式將所有資料在資料第一次存取時就“一次查完“因此在controller的部份我們可以這樣寫:
#products_controller.rb
def index
@products = Product.includes(:user)
end
再 query 的部分就只會查詢2筆了。
:joins 使用 SQL 的 INNER JOIN 方法,不會真的把關聯的資料取出來。如果只是想要篩選結果,或是觀察關聯物件的某些屬性質,那麼使用 :joins 是最有效率的。不過有一點要注意,如果你想要做的事是存取關聯物件本身,那麼 :joins 還是會造成 N+1 問題。
主要差別在於:
參考資料:
部分內容擷取自以下連結
Preload, Eagerload, Includes and Joins
Rails使用include和join避免 N+1 queries
[Rails] N+1 Queries Problem
Rails API
“Tests are a gift. And great tests are a great gift. To fail the test is a misfortune. But to refuse the test is to refuse the gift, and something worse, more >irrevocable, than misfortune.”
– Lois McMaster Bujold, writer
本文同步發佈於: https://louiswuyj.tw/